﻿namespace SpikingAnalyticsLib

open System
open System.IO
open System.Collections.Generic
open SpikingNeuronLib

// ===============================================================================================================
// General utility functions

module GeneralUtilities =

    let CreateNullablePath folder fileName =
        if folder = null || fileName = null then
            null
        else Path.Combine(folder, fileName)

    let CreateOptionFromNullable item =
        if item = null then
            None
        else
            Some(item)

    /// Turn a sequence of items into a string with items separated by the specified character
    // A faster version of Stringify
    let StringifyWithSeparator separator (list:seq<'a>) =
        System.String.Join(separator, list)

    /// Transform a list of selected neurons into a boolean mask over ALL neuron indices
    let CreateFilter selected count =
        let result = [| for i in 1..count do yield false |]
        selected
        |> Seq.iter (fun index -> result.[index] <- true)
        result

    /// Create a map where the specified number of keys (slots) have been pre-allocated with the specified default value
    let PreAllocateMap slots defaultValue =
        Map.ofSeq (seq { for i in 0..slots-1 -> i, defaultValue })

    //----------------------------------------------------------------------
    // Convert a Map to a Dictionary

    let ConvertToDictionary (map:Map<'a, 'b>) =
        map
        |> Map.fold (fun (state:Dictionary<'a, 'b>) key value ->
                state.Add(key, value)
                state
            ) (new Dictionary<'a, 'b>())

//----------------------------------------------------------------------
// Group multiple consecutive lines of data together e.g. read from a file
// Because the lines are handled as strings, the line format does not need to be the same for each line in the group
// e.g. use 'GroupLines 4 lines' on the following 4-line file format to produce a keyed sequence of 4-element arrays:
// 1 2 3
// Some info...
// (10, 5), (20, 10),
// ======================================================================

    let GroupLines groupSize (lines:seq<string>) =
        lines
        |> Seq.mapi (fun index line -> (index / groupSize), line)       // give each line an index that groups groupSize elements together
        |> Seq.groupBy (fun (index, line) -> index)
        |> Seq.map (fun (key, data) ->
                let lines =
                    data
                    |> Seq.map (fun (index, line) -> line)
                    |> Seq.toArray
                key, lines
            )

//----------------------------------------------------------------------
// General maps of the form Map<'a, 'b> and IDictionary<'a, 'b>
// or the form Map<'a, 'b list> and IDictionary<'a, IEnumerable<'b>>

    let ConvertMapToDictionary (map:Map<'a, 'b>) =
        let dict = new Dictionary<'a, 'b>(map.Count)
        map
        |> Map.toSeq
        |> Seq.iter (fun (key, value) -> dict.Add(key, value))

        dict

    let ConvertMappedValues (valueConverter:'b -> 'c) (map:Map<'a, 'b>) =
        map
        |> Map.map (fun key value -> valueConverter value)

    let ConvertListToEnumerable (itemList:'a list) =
        itemList |> List.toSeq

//----------------------------------------------------------------------
// PatternMaps of the form Map<int, Pattern option> and IDictionary<int, Pattern>

    let ConvertToPatternMap (inputPatterns:IDictionary<int, IStimulus>) =
        inputPatterns
        |> Seq.map (fun kvp ->
                let pattern =
                    if kvp.Value = null then
                        None
                    else
                        Some(kvp.Value)
                kvp.Key, pattern
            )
        |> Map.ofSeq

//----------------------------------------------------------------------
// Make all combinations of the given sequences
// Replaces nested for loops like the following with a function call
// e.g.
//  for i in 0..20 do
//    for j in 0..20 do
//      for k in 0..20 do -> i, j, k
// e.g. let item = [| for i in 0..20 -> i|]
// e.g. let items = [| item; item; item; |]
// MakeCombinations items -> triplet combinations of integers 0-20
    let MakeCombinations (items:int[][]) =
        let rec MakeCombinations (items:int[][]) level maxDepth =
            let row = items.[level]
            if level < maxDepth then
                let result =
                    MakeCombinations items (level + 1) maxDepth
                    |> Seq.toArray
                seq {
                    for i in row do
                        for j in result do
                            yield i :: j
                }
            else
                seq {
                    for i in row do
                        yield [ i; ]
                }
        let maxDepth = items.Length - 1
        MakeCombinations items 0 maxDepth

//----------------------------------------------------------------------
// Generate combinations of weights that increment up to the maximum combined weight
// e.g. 3 input neurons onto a single output upto max. weight = 30.0 mV
//> GenerateWeightTrials 3 30.0;;
//val it : (float * float list) list =
//  [(17.0, [6.0; 6.0; 5.0]); (18.0, [6.0; 6.0; 6.0]); (19.0, [7.0; 6.0; 6.0]);
//   (20.0, [7.0; 7.0; 6.0]); (21.0, [7.0; 7.0; 7.0]); (22.0, [8.0; 7.0; 7.0]);
//   (23.0, [8.0; 8.0; 7.0]); (24.0, [8.0; 8.0; 8.0]); (25.0, [9.0; 8.0; 8.0]);
//   (26.0, [9.0; 9.0; 8.0]); (27.0, [9.0; 9.0; 9.0]); (28.0, [10.0; 9.0; 9.0]);
//   (29.0, [10.0; 10.0; 9.0]); (30.0, [10.0; 10.0; 10.0])]
    let GenerateWeightTrials numberOfInputs maxCombinedWeights =
        assert (numberOfInputs >= 2)

        seq {
            let baseList =
                // get weights that, when combined, are between 17 and the maximum combined weight
                Seq.unfold (fun state ->
                    let combined = state * numberOfInputs
                    if combined <= int maxCombinedWeights then
                        Some(state, state + 1)
                    else
                        None
                ) 1
                |> Seq.filter (fun combined -> (combined + 1) * numberOfInputs >= 17)
                |> Seq.toList
            let nudgeList =
                // create lists of small nudges of the required length
                seq {
                    for i in 1..numberOfInputs do
                        let nudges = Array.zeroCreate numberOfInputs
                        let runLength = numberOfInputs - i
                        for j in runLength-1..(-1)..0 do
                            nudges.[j] <- 1
                        yield nudges
                }
                |> Seq.fold (fun state nudges -> nudges :: state) []
        
            for i in baseList do
                for nudges in nudgeList do
                    let combined =
                        nudges
                        |> Seq.map (fun nudge -> float (i + nudge))
                        |> Seq.toList
                    let sum = Seq.sum combined
                    if sum >= 17.0 && sum <= maxCombinedWeights then
                        yield sum, combined
        }
        |> Seq.toList

//----------------------------------------------------------------------
// Generate a random sequence of the specified length with no repeats
// i.e. given an incrementing sequence from 0..length-1
// return the sequence in a random order

    let GenerateRandomSequence(length) =
        let inclusiveMinimum, exclusiveMaximum = 0, length

        // sample in this range without replacement
        let rnd = new Random(int(DateTime.Now.ToFileTimeUtc()))
        Seq.unfold (fun (state:Set<int>) ->
            if state.Count >= length then
                None
            else
                let index = rnd.Next(inclusiveMinimum, exclusiveMaximum)
                let newValue, newState =
                    if state.Contains index then
                        None, state
                    else
                        Some(index), state.Add index
                Some(newValue, newState)
        ) Set.empty
        |> Seq.choose id
        |> Seq.toList

//----------------------------------------------------------------------
// Generate all combinations of size 'groupSize' from a group of size 'numElementsToCombine'
// Computationally equivalent to the following (where groupSize is fixed at 3):
//    for i in 0..numElementsToCombine-1 do
//        for j in i..numElementsToCombine-1 do
//            for k in j..numElementsToCombine-1 do
//                if i <> j && j <> k then
//                    yield i, j, k

    let GetCombinations groupSize numElementsToCombine =

        let MakeArray groupSize =
            let combinations = Array.init groupSize (fun index -> index)
            Some(combinations)

        let GetNextIndexCombinations numElementsToCombine indexCombinations =

            let rec Increment1 (combinations:int []) i =
                let index = groupSize - i
                combinations.[index] <- combinations.[index] + 1
                if combinations.[index] > numElementsToCombine - i then
                    if i >= groupSize then
                        None, i + 1
                    else
                        Increment1 combinations (i + 1)
                else
                    Some(combinations), i

            let rec Increment2 (combinations:int []) i =
                if i <= 1 then
                    Some(combinations), i
                else
                    combinations.[groupSize - i + 1] <- combinations.[groupSize - i] + 1
                    Increment2 combinations (i - 1)

            let combinations, i = Increment1 (Array.copy indexCombinations) 1
            let result, _ =
                if combinations.IsSome then
                    Increment2 combinations.Value i
                else
                    combinations, i
            result

        Seq.unfold (fun (state:(int []) option) ->
            if state.IsSome then
                let result = GetNextIndexCombinations numElementsToCombine state.Value
                Some(state, result)
            else
                None
        ) (MakeArray groupSize)
        |> Seq.choose id

//----------------------------------------------------------------------
// Given an ordered list of index mappings, fold them into a single mapping in the specified order
// Index mappings map one integer index onto another integer index
// Used to reorder neuron indexes when plotting PNGs, clustering particular groups of neurons together
// e.g. neurons that fire due to different triggers

    let GenerateClusteredIndexMapping (partitionedMappings:Map<int, int> list) =

        let Remap oldMap initialMap =
            oldMap
            |> Map.fold (fun (state:Map<int, int>) neuron _ ->
                Map.add neuron state.Count state
            ) initialMap

        partitionedMappings
        |> Seq.fold (fun state mapping ->
            Remap mapping state
        ) Map.empty

//----------------------------------------------------------------------
